} = constants;
const pathModule = require('path');
-const { isAbsolute } = pathModule;
const { isArrayBufferView } = require('internal/util/types');
const binding = internalBinding('fs');
const type = (typeof type_ === 'string' ? type_ : null);
const callback = makeCallback(arguments[arguments.length - 1]);
- if (permission.isEnabled()) {
- // The permission model's security guarantees fall apart in the presence of
- // relative symbolic links. Thus, we have to prevent their creation.
- if (BufferIsBuffer(target)) {
- if (!isAbsolute(BufferToString(target))) {
- callback(new ERR_ACCESS_DENIED('relative symbolic link target'));
- return;
- }
- } else if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) {
- callback(new ERR_ACCESS_DENIED('relative symbolic link target'));
- return;
- }
+ // Due to the nature of Node.js runtime, symlinks has different edge cases that can bypass
+ // the permission model security guarantees. Thus, this API is disabled unless fs.read
+ // and fs.write permission has been given.
+ if (permission.isEnabled() && !permission.has('fs')) {
+ callback(new ERR_ACCESS_DENIED('fs.symlink API requires full fs.read and fs.write permissions.'));
+ return;
}
target = getValidatedPath(target, 'target');
}
}
- if (permission.isEnabled()) {
- // The permission model's security guarantees fall apart in the presence of
- // relative symbolic links. Thus, we have to prevent their creation.
- if (BufferIsBuffer(target)) {
- if (!isAbsolute(BufferToString(target))) {
- throw new ERR_ACCESS_DENIED('relative symbolic link target');
- }
- } else if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) {
- throw new ERR_ACCESS_DENIED('relative symbolic link target');
- }
+ // Due to the nature of Node.js runtime, symlinks has different edge cases that can bypass
+ // the permission model security guarantees. Thus, this API is disabled unless fs.read
+ // and fs.write permission has been given.
+ if (permission.isEnabled() && !permission.has('fs')) {
+ throw new ERR_ACCESS_DENIED('fs.symlink API requires full fs.read and fs.write permissions.');
}
target = getValidatedPath(target, 'target');
Symbol,
Uint8Array,
FunctionPrototypeBind,
- uncurryThis,
} = primordials;
const { fs: constants } = internalBinding('constants');
const binding = internalBinding('fs');
const { Buffer } = require('buffer');
-const { isBuffer: BufferIsBuffer } = Buffer;
-const BufferToString = uncurryThis(Buffer.prototype.toString);
const {
codes: {
kValidateObjectAllowNullable,
} = require('internal/validators');
const pathModule = require('path');
-const { isAbsolute } = pathModule;
-const { toPathIfFileURL } = require('internal/url');
const {
kEmptyObject,
lazyDOMException,
}
}
- if (permission.isEnabled()) {
- // The permission model's security guarantees fall apart in the presence of
- // relative symbolic links. Thus, we have to prevent their creation.
- if (BufferIsBuffer(target)) {
- if (!isAbsolute(BufferToString(target))) {
- throw new ERR_ACCESS_DENIED('relative symbolic link target');
- }
- } else if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) {
- throw new ERR_ACCESS_DENIED('relative symbolic link target');
- }
+ // Due to the nature of Node.js runtime, symlinks has different edge cases that can bypass
+ // the permission model security guarantees. Thus, this API is disabled unless fs.read
+ // and fs.write permission has been given.
+ if (permission.isEnabled() && !permission.has('fs')) {
+ throw new ERR_ACCESS_DENIED('fs.symlink API requires full fs.read and fs.write permissions.');
}
target = getValidatedPath(target, 'target');
fs.symlinkSync(path.join(readOnlyFolder, 'file'), path.join(readWriteFolder, 'link-to-read-only'), 'file');
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
- permission: 'FileSystemWrite',
- resource: path.toNamespacedPath(path.join(readOnlyFolder, 'file')),
+ message: 'fs.symlink API requires full fs.read and fs.write permissions.',
}));
assert.throws(() => {
fs.linkSync(path.join(readOnlyFolder, 'file'), path.join(readWriteFolder, 'link-to-read-only'));
resource: path.toNamespacedPath(path.join(readOnlyFolder, 'file')),
}));
- // App will be able to symlink to a writeOnlyFolder
- fs.symlink(path.join(readWriteFolder, 'file'), path.join(writeOnlyFolder, 'link-to-read-write'), 'file', (err) => {
- assert.ifError(err);
- // App will won't be able to read the symlink
- fs.readFile(path.join(writeOnlyFolder, 'link-to-read-write'), common.expectsError({
- code: 'ERR_ACCESS_DENIED',
- permission: 'FileSystemRead',
- }));
-
- // App will be able to write to the symlink
- fs.writeFile(path.join(writeOnlyFolder, 'link-to-read-write'), 'some content', common.mustSucceed());
- });
fs.link(path.join(readWriteFolder, 'file'), path.join(writeOnlyFolder, 'link-to-read-write2'), (err) => {
assert.ifError(err);
// App will won't be able to read the link
fs.symlinkSync(path.join(readWriteFolder, 'file'), path.join(readOnlyFolder, 'link-to-read-only'), 'file');
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
- permission: 'FileSystemWrite',
- resource: path.toNamespacedPath(path.join(readOnlyFolder, 'link-to-read-only')),
+ message: 'fs.symlink API requires full fs.read and fs.write permissions.',
}));
assert.throws(() => {
fs.linkSync(path.join(readWriteFolder, 'file'), path.join(readOnlyFolder, 'link-to-read-only'));
fs.readFileSync(blockedFile);
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
- permission: 'FileSystemRead',
}));
assert.throws(() => {
fs.appendFileSync(blockedFile, 'data');
fs.symlinkSync(regularFile, blockedFolder + '/asdf', 'file');
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
- permission: 'FileSystemWrite',
}));
assert.throws(() => {
fs.linkSync(regularFile, blockedFolder + '/asdf');
fs.symlinkSync(blockedFile, path.join(__dirname, '/asdf'), 'file');
}, common.expectsError({
code: 'ERR_ACCESS_DENIED',
- permission: 'FileSystemRead',
}));
assert.throws(() => {
fs.linkSync(blockedFile, path.join(__dirname, '/asdf'));
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
}));
+}
+
+// fs.symlink API is blocked by default
+{
+ assert.throws(() => {
+ fs.symlinkSync(regularFile, regularFile);
+ }, common.expectsError({
+ message: 'fs.symlink API requires full fs.read and fs.write permissions.',
+ code: 'ERR_ACCESS_DENIED',
+ }));
+
+ fs.symlink(regularFile, regularFile, common.expectsError({
+ message: 'fs.symlink API requires full fs.read and fs.write permissions.',
+ code: 'ERR_ACCESS_DENIED',
+ }));
}
\ No newline at end of file
-// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=*
+// Flags: --experimental-permission --allow-fs-read=*
'use strict';
const common = require('../common');
const error = {
code: 'ERR_ACCESS_DENIED',
- message: /relative symbolic link target/,
+ message: /symlink API requires full fs\.read and fs\.write permissions/,
};
for (const targetString of ['a', './b/c', '../d', 'e/../f', 'C:drive-relative', 'ntfs:alternate']) {
}
}
-// Absolute should not throw
+// Absolute should throw too
for (const targetString of [path.resolve('.')]) {
for (const target of [targetString, Buffer.from(targetString)]) {
for (const path of [__filename]) {
symlink(target, path, common.mustCall((err) => {
assert(err);
- assert.strictEqual(err.code, 'EEXIST');
- assert.match(err.message, /file already exists/);
+ assert.strictEqual(err.code, error.code);
+ assert.match(err.message, error.message);
}));
}
}
const blockedFile = fixtures.path('permission', 'deny', 'protected-file.md');
const blockedFolder = tmpdir.resolve('subdirectory');
const symlinkFromBlockedFile = tmpdir.resolve('example-symlink.md');
+const allowedFolder = tmpdir.resolve('allowed-folder');
+const traversalSymlink = path.join(allowedFolder, 'deep1', 'deep2', 'deep3', 'gotcha');
{
tmpdir.refresh();
fs.mkdirSync(blockedFolder);
+ // Create deep directory structure for path traversal test
+ fs.mkdirSync(allowedFolder);
+ fs.writeFileSync(path.resolve(allowedFolder, '../protected-file.md'), 'protected');
+ fs.mkdirSync(path.join(allowedFolder, 'deep1'));
+ fs.mkdirSync(path.join(allowedFolder, 'deep1', 'deep2'));
+ fs.mkdirSync(path.join(allowedFolder, 'deep1', 'deep2', 'deep3'));
}
{
// Symlink previously created
+ // fs.symlink API is allowed when full-read and full-write access
fs.symlinkSync(blockedFile, symlinkFromBlockedFile);
+ // Create symlink for path traversal test - symlink points to parent directory
+ fs.symlinkSync(allowedFolder, traversalSymlink);
}
{
[
'--experimental-permission',
`--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, `--allow-fs-read=${symlinkFromBlockedFile}`,
+ `--allow-fs-read=${allowedFolder}`,
`--allow-fs-write=${symlinkFromBlockedFile}`,
file,
],
BLOCKEDFOLDER: blockedFolder,
BLOCKEDFILE: blockedFile,
EXISTINGSYMLINK: symlinkFromBlockedFile,
+ TRAVERSALSYMLINK: traversalSymlink,
+ ALLOWEDFOLDER: allowedFolder,
},
}
);